查看原文
其他

通过性能指标学习Linux Kernel - (下)

赵晨雨 Linux内核之旅 2022-12-14


作者介绍:

赵晨雨,师从陈莉君教授,Linux内核之旅社区maintainer,现就职于thoughtworks 安全与系统研发事业部,thoughtworks未济实验室成员。



B站录屏链接:

GLCC编程夏令营——LMP课题 周会分享

https://www.bilibili.com/video/BV1oY4y177Wh?spm_id_from=333.337.search-card.all.click


上期回顾:

通过性能指标学习Linux Kernel - (上)


上期我们介绍了atop和proc统计调度延迟的原理,内核还存在很多的基础设施,这些基础设施都是强有⼒的⼯具,我们最终是要落地到 eBPF 中的,在 eBPF 中我个⼈认为关键事件是很关键的⼀环,因为eBPF太精准了,⽽它的精准是精准在内核中各个事件上。

 

3. tracepoint

tracepoint是linux kernel中的静态探针,是内核中天然的关键事件集合,这些静态探针点是在linux kernel代码中硬编码的,并且范围也非常广,大约有1800多个事件点,例如系统调用的入口和出口、调度事件、文件系统操作等等,有一个非常好的优点是接口稳定。

查看系统中可以使用的tracepoint有关于调度的事件:

$ sudo perf list tracepoint | grep sched:
sched:sched_kthread_stop [Tracepoint event]
sched:sched_kthread_stop_ret [Tracepoint event]
sched:sched_kthread_work_execute_end [Tracepoint event]
sched:sched_kthread_work_execute_start [Tracepoint event]
sched:sched_kthread_work_queue_work [Tracepoint event]
sched:sched_migrate_task [Tracepoint event]
sched:sched_move_numa [Tracepoint event]
sched:sched_pi_setprio [Tracepoint event]
sched:sched_process_exec [Tracepoint event]
sched:sched_process_exit [Tracepoint event]
sched:sched_process_fork [Tracepoint event]
sched:sched_process_free [Tracepoint event]
sched:sched_process_hang [Tracepoint event]
sched:sched_process_wait [Tracepoint event]
sched:sched_stat_blocked [Tracepoint event]
sched:sched_stat_iowait [Tracepoint event]
sched:sched_stat_runtime [Tracepoint event]
sched:sched_stat_sleep [Tracepoint event]
sched:sched_stat_wait [Tracepoint event]
sched:sched_stick_numa [Tracepoint event]
sched:sched_swap_numa [Tracepoint event]
sched:sched_switch [Tracepoint event]
sched:sched_wait_task [Tracepoint event]
sched:sched_wake_idle_without_ipi [Tracepoint event]
sched:sched_wakeup [Tracepoint event]
sched:sched_wakeup_new [Tracepoint event]
sched:sched_waking [Tracepoint event]

使用perf工具可以直接查看系统中当前这些事件发生时产生的数据:

$ sudo perf trace -e sched:sched_wakeup

tracepoint是hard coded在Linux kernel代码中的,可以在内核中看一眼:

/** Mark the task runnable and perform wakeup-preemption.*/static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags, struct rq_flags *rf){ check_preempt_curr(rq, p, wake_flags); p->state = TASK_RUNNING; trace_sched_wakeup(p);
#ifdef CONFIG_SMP if (p->sched_class->task_woken) { /*
  • 第9行的代码就是tracepoint事件点,对应 sched:sched_wakeup

查看格式化输出的事件内容:

$ sudo cat /sys/kernel/debug/tracing/events/sched/sched_wakeup/format
name: sched_wakeup

ID: 318

format:

field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;

field:char comm[16]; offset:8; size:16; signed:1;
field:pid_t pid; offset:24; size:4; signed:1;
field:int prio; offset:28; size:4; signed:1;
field:int success; offset:32; size:4; signed:1;

field:int target_cpu; offset:36; size:4; signed:1;


print fmt: "comm=%s pid=%d prio=%d target_cpu=%03d", REC->comm, REC->pid, REC->prio, REC->target_cpu

这些数据输出还有一个作用,就是可以对这些内容执行tracepoint追踪过滤:

$ sudo perf trace -e sched:sched_wakeup --filter 'pid == 6079'
  • 这样就可以只看到内核中pid是6079的task的task_wakeup事件了。

所以可以发现,内核内置的tracepoint事件点已经是非常精确的定位了,并且替我们选择好了关键事件,而且在proc中我们提到的关键事件在 tracepoint 中都有涉及,因此直接利用 tracepoint 也是一种很好的方法。

3.1 利用tracepoint定位内核关键事件的源码位置

在proc的指标提取原理分析中,我们定位到的是proc的计算点,但是还没有看到具体的内核调度器代码,这里我们利用 tracepoint 是可以定位到调度器的核心代码中的,因此现在目标就是定位进入runqueue 的事件和调度下CPU的事件。

tracepoint的函数都有一定的格式,我们可以利用这个格式来快速查找,查找进程 wakeup 的事件点 trace_sched_wakeup();

/** Mark the task runnable and perform wakeup-preemption.*/static void ttwu_do_wakeup(struct rq *rq, struct task_struct *p, int wake_flags, struct rq_flags *rf){ check_preempt_curr(rq, p, wake_flags); p->state = TASK_RUNNING; trace_sched_wakeup(p);

这里直接就定位到了给 task 设置为 TASK_RUNNING 状态的事件点,这里也可以全局搜索 p->state = TASK_RUNNING; 会有另外的发现:

/* * wake_up_new_task - wake up a newly created task for the first time. * * This function will do some initial scheduler statistics housekeeping * that must be done for every newly created context, then puts the task * on the runqueue and wakes it. */void wake_up_new_task(struct task_struct *p){ struct rq_flags rf; struct rq *rq;
raw_spin_lock_irqsave(&p->pi_lock, rf.flags); p->state = TASK_RUNNING;··· post_init_entity_util_avg(p);
activate_task(rq, p, ENQUEUE_NOCLOCK); trace_sched_wakeup_new(p);
  • 第14行又设置了进程状态为 TASK_RUNNING 。

  • 又在第32行发现了嫌疑函数 trace_sched_wakeup_new() 对应的tracepoint事件是 sched:sched_wakeup_new 。

也可以利用这种方法找到调度下CPU的事件,利用tracepoint事件 sched:sched_switch ,那么可以全局搜一下 trace_sched_switch() 函数,就会发现主调度器的代码了:

static void __sched notrace __schedule(bool preempt){ struct task_struct *prev, *next; unsigned long *switch_count; struct rq_flags rf; struct rq *rq; int cpu;··· ++*switch_count;
trace_sched_switch(preempt, prev, next);
/* Also unlocks the rq: */ rq = context_switch(rq, prev, next, &rf);···


  • 就在函数 context_switch 的前面,所以tracepoint是天然的定位代码的神器。

调度器的函数调用关系也可以利用 ftrace 工具来观察,另外也可以使用网站 livegrep<https://link.zhihu.com/?target=https%3A//livegrep.com/>来获得函数调用关系:

ttwu_do_wakeup() --> trace_sched_wakeupwake_up_new_task() --> trace_sched_wakeup_new__schedule() --> trace_sched_switch

 

4. perf的统计方法

那么现有的工具有没有基于 tracepoint 来进行统计的?那就离不开 perf 了, perf基于事件采样原理,以性能事件为基础,除了 tracepoint ,也使用性能监控单元(PMU)来测量、记录和监控各种硬件和软件事件,还支持kprobes 和 uprobes等事件。

perf的原理大致是这样的:每隔一个固定的时间,就在CPU上(每个核上都有)产生一个中断,在中断上看看,当前是哪个pid,哪个函数,然后给对应的pid和函数加一个统计值,这样,我们就知道CPU有百分几的时间在某个pid,或者某个函数上了。

使用 perf 统计过去5s的调度延迟:

$ sudo perf sched record -- sleep 5

然后会生成一个perf.data的文件,然后需要使用如下命令解析:

$ sudo perf sched latency

就可以看到这样的信息:

------------------------------------------------------------------------------------------------------------------------------------------- Task | Runtime ms | Switches | Avg delay ms | Max delay ms | Max delay start | Max delay end | ------------------------------------------------------------------------------------------------------------------------------------------- sh:(5) | 11.075 ms | 5 | avg: 0.128 ms | max: 0.186 ms | max start: 23083.066371 s | max end: 23083.066557 s lpstat:(4) | 35.320 ms | 4 | avg: 0.101 ms | max: 0.116 ms | max start: 23083.068281 s | max end: 23083.068397 s sed:(4) | 8.584 ms | 4 | avg: 0.084 ms | max: 0.100 ms | max start: 23080.926985 s | max end: 23080.927085 s

更彻底一点,把每一次任务切换的信息都展现出来,就更能知道每一次延迟是怎样发生的,而这正是 sudo perf sched timehist 可以完成的事。它可以统计每轮 task switch 时,之前在 CPU 上运行的那个 "prev" 线程得到的执行时间 (run time) ,以及该线程在获得这次执行机会前的休眠态等待 (wait time) 和运行态等待 (sch delay) 时间(这个 patch<https://link.zhihu.com/?target=https%3A//lore.kernel.org/lkml/1479919218-6367-15-git-send-email-acme%40kernel.org/>

Samples do not have callchains. time cpu task name wait time sch delay run time [tid/pid] (msec) (msec) (msec)--------------- ------ ------------------------------ --------- --------- --------- 23078.905190 [0000] perf[207623] 0.000 0.000 0.000 23078.905236 [0000] migration/0[14] 0.000 0.002 0.046 23078.905319 [0001] perf[207623] 0.000 0.000 0.000

而要得到比之再 detail 一点的记录,就该用上 sudo perf sched script 了。它可以展示每次的任务切换具体是怎样发生的:

            perf 207623 [000] 23078.905185: sched:sched_stat_runtime: comm=perf pid=207623 runtime=80289[ns] vruntime=29533570197 [ns] perf 207623 [000] 23078.905187: sched:sched_waking: comm=migration/0 pid=14 prio=0 target_cpu=000 perf 207623 [000] 23078.905189: sched:sched_stat_runtime: comm=perf pid=207623 runtime=4657 [ns] vruntime=29533574854 [ns] perf 207623 [000] 23078.905190: sched:sched_switch: prev_comm=perf prev_pid=207623 prev_prio=120 prev_state=R+ ==> next_comm=migration/0 next_pid=14 next_prio=0 migration/0 14 [000] 23078.905192: sched:sched_migrate_task: comm=perf pid=207623 prio=120 orig_cpu=0 dest_cpu=1 migration/0 14 [000] 23078.905236: sched:sched_switch: prev_comm=migration/0 prev_pid=14 prev_prio=0 prev_state=S ==> next_comm=swapper/0 next_pid=0 next_prio=120

可以直接发现 perf 使用了tracepoint事件,还包括了 _sched_stat_runtime_ 事件,因为它统计了task运行在CPU上的时间。

 

5. eBPF的统计方法—面向task

通过对 atop 提取性能指标的原理和方式我们的结论是提取频率秒级别,并且是快照信息;perf 工具可以直接使用 tracepoint 事件源,并且实现了更为强大的功能,可以捕获到每一次事件的发生,proc 和 tracepoint 的内核统计点都非常精确。

现在的目标是来看看 eBPF 用于提取性能指标的时候,有哪些不同。

5.1 eBPF开发框架及工具选择

eBPF 目前还处于发展期,eBPF 的开发框架也还是多种多样的,不同编程语言也都有相应的开发框架,根据目前我的了解,libbpf 的 rust 开发框架和 libbpf 的 c 开发框架是反馈比较好的,而如果说目前最推荐的eBPF性能项目我个人依然认为是 bcc 和 bpftrace ,如果逻辑较为简单可以采用bpftrace ,如果要求可编程性可以参考 bcc

好多同学对bcc的印象是它是一个 python 前端的 eBPF 开发框架,但是不是的,bcc 官网介绍是:

BCC is a toolkit for creating efficient kernel tracing and manipulation programs, and includes several useful tools and examples. It makes use of extended BPF (Berkeley Packet Filters), formally known as eBPF, a new feature that was first added to Linux 3.15. Much of what BCC uses requires Linux 4.1 and above.

python只是开发工具使用的一个前端框架而已,并且bcc社区目前对之前的很多工具已经进行了libbpf迁移,位于 [libbpf-tools](https://github.com/iovisor/bcc/tree/master/libbpf-tools) 目录下,并且支持 x86 powerpc arm64

接下来说下目前eBPF开发的方式,bcc下的 [libbpf-tools](https://github.com/iovisor/bcc/tree/master/libbpf-tools) 目录已经是一个比较完善的环境了,可以直接进行开发,也可以采用 libbpf-bootstrap 项目的方式进行开发,这两种方式都需要安装llvm和clang。

目前使用 eBPF 有一个非常方便或者说是必备的工具 bpftool ,利用这个工具可以了解当前系统有关于eBPF的配置选项、系统限制、可以利用哪些种类的maps、每种 eBPF 程序类型可以使用哪些 helpers ,并且会告诉你哪些类型的 eBPF 程序类型不可以使用:

$ sudo bpftool feature

5.2 eBPF的统计方法

我们的目标是介绍 eBPF 提取数据的原理,因此不展开讲述如何进行 eBPF 编程。
介绍了前面的内容,现在来介绍eBPF提取的原理就方便很多了,一句话总结就是 原理 + tracepoint的事件频率 + 可编程性

为了更直接地说明原理,我们采用 bpftrace 代码,会更为清晰:

tracepoint:sched:sched_wakeup,tracepoint:sched:sched_wakeup_new{ @qtime[args->pid] = nsecs;}
tracepoint:sched:sched_switch{ if (args->prev_state == TASK_RUNNING) { @qtime[args->prev_pid] = nsecs; }
$ns = @qtime[args->next_pid]; if ($ns) { @usecs = hist((nsecs - $ns) / 1000); } delete(@qtime[args->next_pid]);}

原理已经很清晰,结合在 tracepoint 部分介绍的原理就可以很容易理解,但是前提是理解了原理,除了bpftrace,还有 bcc 项目,当我们在工作中需要利用 eBPF 的可编程性的时候,很有参考价值,可以把 bcc 中的每一个工具都看为是一个模板。

现在来看 bcc 中的调度延迟:

SEC("tp_btf/sched_wakeup")int BPF_PROG(sched_wakeup, struct task_struct *p){ if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0)) return 0;
return trace_enqueue(p->tgid, p->pid);}
SEC("tp_btf/sched_wakeup_new")int BPF_PROG(sched_wakeup_new, struct task_struct *p){ if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0)) return 0;
return trace_enqueue(p->tgid, p->pid);}

filter_cg 是打开过滤开关,我们没有启用因此可以忽略,所以在一个 task 进入runqueue 的时候,只干了一件事情,那就是记录该 task 的 tgid 和 pid 。

那么什么是 tgid 和 pid?内核的 task_struct 中的 pid 一定是全局唯一的,什么意思?就是用户态下一个进程 fork 一个进程出来,那么这两个进程的 pid 是不同的,用户态下一个进程产生一个线程出来,那么这个线程的pid和进程的pid也是不同的;那么怎么知道一个线程是哪一个进程的?就是通过tgid,一个进程A产生了一个线程B,那么A和B有不同的pid,但是B的tgid等于A的pid。

trace_enqueue()函数只做了一件事情,就是记录当前这个pid进程进入 runqueue 的时间戳, 现在只考虑最普通的情况,只记录pid的情况,因此每有一个 task 被加入到 runqueue 的时候,就记录这个 task 的 pid 和当前的纳秒时间戳。

再来看看将进程调度下CPU的事件:

SEC("tp_btf/sched_switch") int BPF_PROG(sched_swith, bool preempt, struct task_struct *prev, struct task_struct *next){ struct hist *histp; u64 *tsp, slot; u32 pid, hkey; s64 delta;··· if (get_task_state(prev) == TASK_RUNNING) trace_enqueue(prev->tgid, prev->pid);
pid = next->pid;
tsp = bpf_map_lookup_elem(&start, &pid); if (!tsp) return 0; delta = bpf_ktime_get_ns() - *tsp; if (delta < 0) goto cleanup;
··· if (targ_ms) delta /= 1000000U; else delta /= 1000U; slot = log2l(delta);···
cleanup: bpf_map_delete_elem(&start, &pid); return 0;}

这里省略了一些数据存储和展示的代码片段,第10行的代码就是在记录被动切换下runqueue 的 task 的时间戳,原理上相信大家已经比较清晰,但是有一行并不起眼的代码就是滴23行还是到26行,这里只是区分了一下不同的时间单位,但是实质上是进行了数据处理,包括27行也是在处理数据,因此 eBPF 提供了内核态下数据预处理的能力,除此之外,利用 eBPF 提供的 map 可以实现各种缓存。

另外再 perf 工具的介绍中,发现 perf 可以利用 tracepoint 拿到很多的信息并且做后续的处理,但是 tracepoint 提供的信息是很有限的,在 tracepoint 中的介绍中也进行了展示,而在上面这个 eBPF 函数中,我们直接拿到了即将被调度下 CPU 的 task_struct 和 下一个即将上 CPU 的 task 的 task_struct,在允许的范围内,我们可以拿到比 perf 多的多的 task 的内部信息。

最重要的是 eBPF 具有可编程性,上面的函数中只是进行了数据预处理、条件筛选的功能,更进一步,在数据来源和频率都很充足的前提下,可以加入每位同学自己不同的逻辑,在每一个事件点上都可以实现更为复杂的逻辑,再进一步,有了可编程能力,可以同时利用不同的事件点,并且不同的事件可以相互作用,激发出不同的效果,这也是 eBPF 可以发挥想象力的地方。


小结

这次分享主要分享了通过传统工具、proc、tracepoint来定位内核代码,另外通过对 atop、perf、eBPF的比较,可以得出一些在这个场景下 eBPF 的一些优势:

  1. 可以利用现有基础设施的优点;

  2. 强大的可编程性;

  3. 可以访问任何受控范围内的字段;

  4. 定制化,之前的基础设施很大程度上是固定的逻辑,我们无法改变或者改变的成本很高,但是eBPF可以给我们提供定制化、自定义的能力;



(完)

由于作者水平有限,本文错漏缺点在所难免,希望读者批评指正。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存